Appearance
场景
储存所有几何体的对象
js
// 背景色
scene.background = new THREE.Color(0xcccccc);
// 环境贴图(将作用所有物体,常用于让模型被打亮)
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
const rgbeLoader = new RGBELoader();
rgbeLoader.load("/public/Alex_Hart-Nature_Lab_Bones_2k.hdr", (envMap) => {
envMap.mapping = THREE.EquirectangularReflectionMapping; // 球面全景图映射
scene.environment = envMap; // 设置场景环境贴图
});快速上手
注:外部的模型,一律放在public静态资源文件夹里,一般路径是/public/模型文件
基本模型
基本图形:通过内置构造器创建
js
const geometry = new THREE.BoxGeometry(1, 1); // 几何体
const material = new THREE.MeshBasicMaterial({ color: "#6cf" }); // 材质
const cube = new THREE.Mesh(geometry, material); // 几何体+材质特殊模型:雾
以摄像头为起点,到一定距离之后开始起雾
js
scene.background = new THREE.Color(0xcccccc); // 建议背景颜色和雾相近
scene.fog = new THREE.Fog(0xcccccc, 0.1, 50); // 线性雾 (颜色, 起效距离, 纯雾距离)
scene.fog = new THREE.FogExp2(0xcccccc, 0.05); // 指数雾 (颜色,指数)外部模型:glb
js
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const gltfLoader = new GLTFLoader();
gltfLoader.load("/public/Duck.glb", (root) => scene.add(root.scene));外部模型:glb(压缩)
额外步骤:将依赖包复制到静态目录
js
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const dracoLoader = new DRACOLoader();
const gltfLoader = new GLTFLoader();
// 解析文件,在three依赖包里,路径:node_modules/three/examples/jsm/libs/draco
dracoLoader.setDecoderPath("./draco/"); // 将整个文件复制到静态目录
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load("/public/city.glb", (root) => scene.add(root.scene));属性
模型
常用属性
js
visible: boolean; // 是否隐藏变形
js
rotation: Vector3; // 旋转,180度是 Math.PI * 2
position: Euler; // 位移,相对于父元素的坐标(scene子项的相对坐标就是世界坐标)
scale: Vector3; // 缩放
// type
Vector3(x:float,y:float,z:float); // 三维向量
Euler(x:float,y:float,z:float,order:string); // 欧拉角,order旋转顺序,默认'XYZ'
// Fn
属性.set(number,number,number) // 修改
属性.x = number // 修改单轴常用几何构造器
js
// 共用一个材质
const material = new THREE.MeshBasicMaterial({
color: "#6cf",
wireframe: true,
});
// 平面
{
// x长度: number,y长度: number, x细分面: number, y细分面: number
const geometry = new THREE.PlaneGeometry(1, 1);
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}
// 正方体
{
// x轴长度: number, y轴长度: number, z轴长度: number, ?x轴分段数: number, ?y轴分段数: number, ?z轴分段数: number
const geometry = new THREE.BoxGeometry(1, 1, 1, 2);
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
}
// 圆形
{
// 半径: number, 分段数: number, 起始角度(2π)?: number, 展开角度(2π)?: number
const geometry = new THREE.CircleGeometry(5, 32, 0, 3.14);
const circle = new THREE.Mesh(geometry, material);
scene.add(circle);
}
// 圆锥
{
// 半径: number,y轴高: number,圆面细分: number,椎体截面细分?: number, 是否隐藏圆面?: boolean, 起始角度(2π)?: number, 展开角度(2π)?: number
const geometry = new THREE.ConeGeometry(5, 5, 32, 10, true);
const cone = new THREE.Mesh(geometry, material);
scene.add(cone);
}
// 圆柱
{
// 顶面半径: number,底面半径: number,y轴高: number,圆面细分: number,椎体截面细分?: number, 是否隐藏圆面?: boolean 起始角度(2π)?: number, 展开角度(2π)?: number
const geometry = new THREE.CylinderGeometry(1, 3, 5, 10);
const cylinder = new THREE.Mesh(geometry, material);
scene.add(cylinder);
}
// 球体
{
// 半径: number,圆面细分: number, 截面细分: number, 起始角度(2π)?: number, 展开角度(2π)?: number,垂直面起始角度(π)?: number, 展开角度(π)?: number
const geometry = new THREE.SphereGeometry(3, 30, 1);
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
}
// 球体(8面体)
{
// 半径: number,分段数?: number
const geometry = new THREE.TetrahedronGeometry(2, 1);
const tetrahedron = new THREE.Mesh(geometry, material);
scene.add(tetrahedron);
}
// 球体(8面体)
{
// 半径: number,分段数?: number
const geometry = new THREE.OctahedronGeometry(2);
const octahedron = new THREE.Mesh(geometry, material);
scene.add(octahedron);
}
// 球体(12面体)
{
// 半径: number,分段数?: number
const geometry = new THREE.DodecahedronGeometry(2);
const dodecahedron = new THREE.Mesh(geometry, material);
scene.add(dodecahedron);
}
// 球体(20面体)
{
// 半径: number,分段数?: number
const geometry = new THREE.IcosahedronGeometry(2);
const icosahedron = new THREE.Mesh(geometry, material);
scene.add(icosahedron);
}
// 环形
{
// 内径: number, 外径: number, 圆面分段: number, 树根分轮: number, 起始角度(2π)?: number, 展开角度(2π)?: number
const geometry = new THREE.RingGeometry(1, 3, 20, 5);
const ring = new THREE.Mesh(geometry, material);
scene.add(ring);
}
// 圆环(甜甜圈)
{
// 圆半径: number, 环半径: number, 圆分段?: number, 环分段?: number, 展开角度(2π)?: number
const geometry = new THREE.TorusGeometry(3, 1, 30, 30);
const torus = new THREE.Mesh(geometry, material);
scene.add(torus);
}材质
js
const material = new THREE.MeshBasicMaterial();
material.color = "#fff"; // 基本材质_颜色
material.transparent = true; // 允许透明度
material.side = THREE.DoubleSide, // 两面可见
material.wireframe = true, // 线框模式
// 加载外部纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/public/texture/材质");
material.map = texture; // 材质
texture.colorSpace = THREE.SRGBColorSpace; // 色彩空间_sRGB
// AO贴图: 正片叠底(暗部闭塞叠加)
const aoMap = textureLoader.load("/public/texture/AO贴图");
material.aoMap = aoMap;
material.aoMapIntensity = 0.5; // ao贴图强度
// alpha贴图: 蒙版(黑透明,白不透明)
const alphaMap = textureLoader.load("/public/texture/alpha贴图");
material.alphaMap = alphaMap;
// 光照贴图: 颜色叠加
const lightMap = textureLoader.load("/public/texture/光照贴图");
material.lightMap = lightMap;
// 环境贴图: 镜面反射
const rgbeLoader = new RGBELoader();
rgbeLoader.load(
"/public/texture/Alex_Hart-Nature_Lab_Bones_2k.hdr",
(envMap) => {
envMap.mapping = THREE.EquirectangularReflectionMapping; // 球面全景图映射
// scene.background = envMap; // 可设置成vr场景
// scene.environment = envMap;
material.envMap = envMap;
material.reflectivity = 0.1; // 反射强度
}
);
// 高光贴图:反射度蒙版
const specularMap = textureLoader.load("/public/texture/高光贴图");
material.specularMap = specularMap;材质属性
js
wireframe: boolean; // 显示为线框组件
OrbitControls轨道控制器
new OrbitControls();
通过监听DOM的鼠标事件,控制摄像机移动 OrbitControls – three.js docs (threejs.org)
参数: - 摄像机 - 被监听的DOM
js
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 5; // 最小缩放
controls.maxDistance = 100; // 最大缩放
controls.autoRotate = true; // 自动旋转
controls.enableDamping = true; // 设置阻尼(惯性)
controls.dampingFactor = 0.001; // 阻尼系数(越小越难推动)
controls.target.set(0, 0, 0); // 视线目标
controls.enablePan = false; // 禁用平移,shift拖拽平移
controls.enableRotate = false; // 禁用旋转,拖拽
controls.enableZoom = false; // 禁用缩放,滚轮
controls.update(); // *更新Raycaster射线
重要,用户和模型交互的纽带 从鼠标位置发出射线,获取被击中的模型
js
// 设靶子环节
const geometry = new THREE.BoxGeometry(10, 10);
const material = new THREE.MeshBasicMaterial({ color: "#fff" });
const cube1 = new THREE.Mesh(geometry, material);
const cube2 = new THREE.Mesh(geometry, material);
const cube3 = new THREE.Mesh(geometry, material);
scene.add(cube1);
scene.add(cube2);
scene.add(cube3);
cube1.position.x = -30;
cube3.position.x = 20;
// 正片
const raycaster = new THREE.Raycaster(); // 射线
const mouse = new THREE.Vector2(); // 存储鼠标向量
// 通过事件触发
window.addEventListener("click", (ev) => {
// 计算,鼠标位于屏幕的位置 -> 平面坐标系的位置(-1到1)
mouse.x = (ev.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera); // 创建射线,从相机到鼠标向量
// 可从场景中检测内容
const intersects = raycaster.intersectObjects(scene.children);
// 可从若干个对象中检测内容
const intersects = raycaster.intersectObjects([cube1, cube2, cube3]);
intersects[n].distance; // number 接触点距离,数组根据该属性从近到远排列
intersects[n].face; // 接触面信息
intersects[n].normal; // 接触点法相向量
intersects[n].object; // *所选物体
intersects[n].point; // 接触点坐标
intersects[n].uv; // 接触uv
});AxesHelper世界坐标辅助器
new THREE.AxesHelper() 参数:坐标线段长度 注:红色X轴,绿色Y轴,蓝色Z轴
js
import * as THREE from "three";
const AxesHelper = new THREE.AxesHelper(5);
scene.add(AxesHelper);GUI属性管理控件
new GUI();
快速创建表单元素,用于控制变量 第一个参数是监听区域,第二个是监听值
实例.add(); 参数: 1. 监听区域 2. 监听值,根据值的类型改变表单类型 3. 简写内容,每一种类型都不同,不推荐用
js
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
const gui = new GUI();
gui.add(...).name(...);
// gui属性
.add(...); // 添加表单项
.name(string); // 表单项label
.onChange((val) => console.log(val)); // 监听变化后的值
.onFinishChange((val) => console.log(val)); // 监听鼠标松开后的值
.min(number); // 最小值,监听数字,如果没有最大值,则显示为数字输入框
.max(number); // 最大值,监听数字,如果有最小值,则显示为滑块
.step(number); // 最小变化单位,滑块
// 文件夹
let box = gui.addFolder("文件夹");
box.add(cube.position, "x").min(-10).max(10).step(1);
box.add(cube.position, "y").min(-10).max(10).step(1);
box.add(cube.position, "z").min(-10).max(10).step(1);
// 按钮:监听内容为【函数】
let eventObj = { // 创建一个方法map
Fullscreen: () => document.body.requestFullscreen(), // 全屏
exitFullscreen: () => document.exitFullscreen() // 退出全屏
};
gui.add(eventObj, "Fullscreen").name("全屏");
gui.add(eventObj, "exitFullscreen").name("退出全屏");;
// 勾选框:监听内容为【布尔值】
const material = new THREE.MeshBasicMaterial();
gui.add(material, "wireframe"); // 监听材质的wirfram属性
// 滑块|输入框:监听内容为【数字】
gui.add(cube.position, "x").min(-10).max(10).step(1);
gui.add(cube.position, "x", -5, 5); // 简写
// 应用: 通过按钮改变颜色
let colorParms = {
cubeColor: "#00ff00",
};
gui
.add(colorParms, "cubeColor") // 监听刚创建的对象
.onChange((val) => { // 改变内容触发事件
cube.material.color.set(val); // 改变材质颜色
});Tween补间动画
额外步骤:
js
import * as TWEEN from "three/examples/jsm/libs/tween.module";
// 施法材料_一个方块
const geometry = new THREE.BoxGeometry(10, 10);
const material = new THREE.MeshBasicMaterial({ color: "#fff" });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 动画fn
const tween = new TWEEN.Tween(cube.position); // 获取动画源
tween.to({ x: 10 }, 1000); // 设置动画值
tween.repeat(Infinity); // 循环
tween.yoyo(true); // 往复,会引起跳帧
tween.delay(50); // 延迟(每次循环),可解决跳帧
tween.easing(TWEEN.Easing.Quadratic.InOut); // 贝塞尔曲线https://tweenjs.github.io/tween.js/examples/03_graphs.html
// 事件
tween.onUpdate((val) => console.log(val)); // 每一帧
tween.onStart((val) => console.log(val)); // 每次(循环)开始
tween.onComplete((val) => console.log(val)); // 每次(循环)完成
tween.onStop((val) => console.log(val)); // 停止(执行.stop)时
// 嵌套,形成往复且不会跳帧
const tween2 = new TWEEN.Tween(cube.position); // 创建动画2
tween2.to({ x: 0 }, 1000);
tween.chain(tween2);
tween2.chain(tween);
// *重要
tween.start(); // 开始动画
// render循环内添加
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
TWEEN.update(); // 下一帧的补间动画
}
animate();加载器(未装填)
js
// 引入
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader"; // hdr全景环境贴图
// 内置
const textureLoader = new THREE.TextureLoader();原理
几何体
几何体由三角形组成,三角形由三个点位组成,每个点位有三个向量(Vector3)
底层属性
js
position: Float32Array; // 用于描述几何体的顶点
index: Uint16Array; // 用于描述顶点的顺序,用于合并重复的公用定点
groups: {start: number, count: number, materialIndex: number}[]; // 给材质分组基本原理
所有几何体都是由三角形拼接而成 2个三角形 => 1个正方形 12个三角形 => 6个面 => 1个正方体
使用顶点,描述正方形
js
// 三角形
{
const geometry = new THREE.BufferGeometry(); // 空几何体
const vertices = new Float32Array([ // 点位顺序:逆时针为正面(反面默认不可见)
-1.0, -1.0, 0.0, // 点位1
1.0, -1.0, 0.0, // 点位2
1.0, 1.0, 0.0, // 点位3
]);
// 给空几何体,添加描述形状的属性,以三个坐标为一个点
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
const material = new THREE.MeshBasicMaterial({
color: "#6cf",
side: THREE.DoubleSide, // 两面可见
wireframe: true, // 线框模式
});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}
// 正方形(三角形拼接)
{
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1.0, -1.0, 0.0, // 三角形1
1.0, -1.0, 0.0,
1.0, 1.0, 0.0,
0.5, 0.5, 0, // 三角形2
-0.5, 0.5, 0,
-0.5, -0.5, 0,
]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
const material = new THREE.MeshBasicMaterial({color: "#6cf"});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}
// 正方形(公用顶点拼接)
{
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1.0, -1.0, 0.0, // 点位1
1.0, -1.0, 0.0, // 点位2
1.0, 1.0, 0.0, // 点位3
-1.0, 1.0, 0.0, // 点位4
]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
// 配置公共顶点,0,1,2组成一个三角形 2,3,0组成一个三角形
const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1)); // 几何体添加索引属性
const material = new THREE.MeshBasicMaterial({color: "#6cf"});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}一个几何体里使用多个材质
几何体的属性groups可用于分组
注:材质为分组时,材质少于几何体内的组,则没被定义的组透明
js
// 顶点分组,使用两种材质
{
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([-1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
// 设置顶点组
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.addGroup(0, 3, 0); // 索引0开始,添加3个顶点,使用材质0
geometry.addGroup(3, 3, 1); // 索引3开始,添加3个顶点,使用材质1
// 配置两种材质
const material1 = new THREE.MeshBasicMaterial({ color: "red" });
const material2 = new THREE.MeshBasicMaterial({ color: "#6cf" });
const plane = new THREE.Mesh(geometry, [material1, material2]); // 材质是数组
scene.add(plane);
}
// 内置几何体的默认分组
{
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material1 = new THREE.MeshBasicMaterial({ color: "red" });
const material2 = new THREE.MeshBasicMaterial({ color: "#6cf" });
// 正方体默认6个组,表示6个面
const cube = new THREE.Mesh(geometry, [
material1,
material2,
material1,
material1,
material1,
material1,
]);
scene.add(cube);
}材质
uv贴图
模型使用uv贴图时,会把贴图列成一个坐标系,→U ↑V 模型的每个顶点都会对应一个坐标,匹配到贴图的位置。
js
// 正方形_顶点拼接
{
const geometry = new THREE.BufferGeometry();
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/public/texture/uv_grid_opengl.jpg");
const vertices = new Float32Array([
-1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0,
]);
const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// 标记uv图的四个点位:左下,右下,右上,左上
const uv = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
// 设置uv属性,用于map材质。两个数字对应一个uv坐标
geometry.setAttribute("uv", new THREE.BufferAttribute(uv, 2));
const material = new THREE.MeshBasicMaterial({ map: texture });
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
plane.position.x = -1;
}
// 正方体_内置几何体
{
const geometry = new THREE.PlaneGeometry(2, 2); // 内置几何体自带uv坐标
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/public/texture/uv_grid_opengl.jpg");
const material = new THREE.MeshBasicMaterial({ map: texture });
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
plane.position.x = 1;
}解决方案
画布自适应窗口
js
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染区宽高
camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机长宽比
camera.updateProjectionMatrix(); // 更新摄像机
}
resize(); // 初始化,让自适应窗口
window.addEventListener("resize", resize); // 监听窗口变化事件全屏按钮
js
renderer.domElement.requestFullscreen(); // 进入全屏,必须挂在在按钮
renderer.domElement.exitFullscreen(); // 退出全屏,必须挂在在按钮
// 游览器API,让元素全屏,不能直接触发,必须挂在在按钮上
Element.requestFullscreen();加载src目录下文件
正常情况下,模型文件放在public静态目录里
js
// 默认加载public静态目录下文件
load('./...')
load('/public/...') // 等同于
// 通过require引入,将基准从public转变为src
load('./asset/...')纹理颜色空间
默认的颜色空间,可能会显得模型发白,这时考虑将色彩空间切换成sRGB
js
// 加载外部纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/public/texture/xx.png");
material.map = texture; // 设置纹理
texture.colorSpace = THREE.SRGBColorSpace; // 色彩空间_sRGB ,更接近人眼直观感受
texture.colorSpace = THREE.LinearSRGBColorSpace; // 色彩空间_线性亮度 ,默认值
// 异步触发
gui
.add(texture, "colorSpace", {
sRGB: THREE.SRGBColorSpace,
colorSpace: THREE.LinearSRGBColorSpace,
})
.onChange(() => {
texture.needsUpdate = true; // 修改必须设置更新
});获取模型尺寸
js
const { min, max } = box3.setFromObject(target); // x,y,z摄像机位置
js
const cameraPosition = new THREE.Vector3(); // 摄像机位置